diff --git a/.rspec_parallel b/.rspec_parallel index 590f731dd1..867e417e06 100644 --- a/.rspec_parallel +++ b/.rspec_parallel @@ -1,3 +1,4 @@ --format progress --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log --tag ~performance diff --git a/Gemfile.lock b/Gemfile.lock index dc49963ec2..eddc27ee84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -328,7 +328,7 @@ GEM kaminari (0.14.1) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - kgio (2.7.4) + kgio (2.9.3) launchy (2.1.2) addressable (~> 2.3) letter_opener (1.0.0) @@ -418,7 +418,7 @@ GEM rake (>= 0.8.7) rdoc (~> 3.4) thor (>= 0.14.6, < 2.0) - raindrops (0.9.0) + raindrops (0.13.0) rake (10.4.2) ransack (0.7.2) actionpack (~> 3.0) @@ -505,7 +505,7 @@ GEM uglifier (1.2.4) execjs (>= 0.3.0) multi_json (>= 1.0.2) - unicorn (4.3.1) + unicorn (4.9.0) kgio (~> 2.6) rack raindrops (~> 0.7) diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index be9948b95d..9c57240dfe 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -14,7 +14,7 @@ .small-12.columns .alert-box.info{ "ofn-inline-alert" => true, ng: { show: "visible" } } %h6 Success! {{ enterprise.name }} added to the Open Food Network - %span If you exit the wizard at any stage, login and go to admin to edit or update your enterprise details. + %span If you exit this wizard at any stage, you need to click the confirmation link in the email you have received. This will take you to your admin interface where you can continue setting up your profile. %a.close{ ng: { click: "close()" } } × .small-12.large-8.columns diff --git a/app/assets/javascripts/templates/registration/finished.html.haml b/app/assets/javascripts/templates/registration/finished.html.haml index f647a2d8bb..46cca9daf5 100644 --- a/app/assets/javascripts/templates/registration/finished.html.haml +++ b/app/assets/javascripts/templates/registration/finished.html.haml @@ -18,7 +18,7 @@ %p We've sent a confirmation email to - %strong {{ enterprise.email }}. + %strong {{ enterprise.email }} if it hasn't been activated before. %br Please follow the instructions there to make your enterprise visible on the Open Food Network. %a.button.primary{ type: "button", href: "/" } Open Food Network home > diff --git a/app/assets/javascripts/templates/registration/introduction.html.haml b/app/assets/javascripts/templates/registration/introduction.html.haml index 60a8547b4a..48553de09a 100644 --- a/app/assets/javascripts/templates/registration/introduction.html.haml +++ b/app/assets/javascripts/templates/registration/introduction.html.haml @@ -5,7 +5,7 @@ %h4 %small %i.ofn-i_040-hub - Create your enterprise profile + You can now create a profile for your Producer or Hub .hide-for-large-up %hr %input.button.small.primary{ type: "button", value: "Let's get started!", ng: { click: "select('details')" } } @@ -38,6 +38,7 @@ %strong contact you on the Open Food Network. %p Use this space to tell the story of your enterprise, to help drive connections to your social and online presence. + %p It's also the first step towards trading on the Open Food Network, or opening an online store. .row.show-for-large-up .small-12.columns diff --git a/app/assets/javascripts/templates/registration/type.html.haml b/app/assets/javascripts/templates/registration/type.html.haml index 48d45cb66a..9593bfa89c 100644 --- a/app/assets/javascripts/templates/registration/type.html.haml +++ b/app/assets/javascripts/templates/registration/type.html.haml @@ -38,9 +38,13 @@ %i.ofn-i_013-help   %p Producers make yummy things to eat &/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it. - / %p Hubs connect the producer to the eater. Hubs can be co-ops, independent retailers, buying groups, wholesalers, CSA box schemes, farm-gate stalls, etc. + .panel.callout + .left + %i.ofn-i_013-help +   + %p If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other. .row.buttons .small-12.columns %input.button.secondary{ type: "button", value: "Back", ng: { click: "select('contact')" } } - %input.button.primary.right{ type: "submit", value: "Continue" } + %input.button.primary.right{ type: "submit", value: "Create Profile" } diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index a0b55ad3c8..e50779c3ff 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + module Admin class EnterprisesController < ResourceController before_filter :load_enterprise_set, :only => :index @@ -199,7 +201,8 @@ module Admin # Overriding method on Spree's resource controller def location_after_save - refered_from_edit = URI(request.referer).path == main_app.edit_admin_enterprise_path(@enterprise) + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + refered_from_edit = referer_path == main_app.edit_admin_enterprise_path(@enterprise) if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53763ad274..aaa7d0bb06 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +require 'open_food_network/referer_parser' + class ApplicationController < ActionController::Base protect_from_forgery @@ -9,7 +11,8 @@ class ApplicationController < ActionController::Base end def set_checkout_redirect - if request.referer and referer_path = URI(request.referer).path + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path session["spree_user_return_to"] = [main_app.checkout_path].include?(referer_path) ? referer_path : root_path end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 1591586f76..5b1bb347a1 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -1,4 +1,5 @@ require 'open_food_network/spree_api_key_loader' +require 'open_food_network/referer_parser' Spree::Admin::ProductsController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader @@ -53,7 +54,8 @@ Spree::Admin::ProductsController.class_eval do protected def location_after_save - if URI(request.referer).path == '/admin/products/bulk_edit' + referer_path = OpenFoodNetwork::RefererParser::path(request.referer) + if referer_path == '/admin/products/bulk_edit' bulk_edit_admin_products_url else location_after_save_original diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 34f2d640c2..81975bb250 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -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 } diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 836080183c..153cc10a82 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -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 diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 4eec0bcd2b..f64f197d56 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -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 diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index d8ea312e23..13ab56c129 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -1,9 +1,7 @@ Spree.user_class.class_eval do - if method_defined? :send_reset_password_instructions_with_delay - Bugsnag.notify RuntimeError.new "send_reset_password_instructions already handled asyncronously - double-calling results in infinite job loop" - else - handle_asynchronously :send_reset_password_instructions - end + # handle_asynchronously will define send_reset_password_instructions_with_delay. + # If handle_asynchronously is called twice, we get an infinite job loop. + handle_asynchronously :send_reset_password_instructions unless method_defined? :send_reset_password_instructions_with_delay has_many :enterprise_roles, :dependent => :destroy has_many :enterprises, through: :enterprise_roles diff --git a/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface new file mode 100644 index 0000000000..548439b60f --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_analytics.html.haml.deface @@ -0,0 +1,3 @@ +/ insert_bottom "[data-hook='admin_footer_scripts']" + += render 'shared/analytics' diff --git a/app/views/enterprise_mailer/confirmation_instructions.html.haml b/app/views/enterprise_mailer/confirmation_instructions.html.haml index e957b70b1c..3fe7ba09e9 100644 --- a/app/views/enterprise_mailer/confirmation_instructions.html.haml +++ b/app/views/enterprise_mailer/confirmation_instructions.html.haml @@ -1,20 +1,22 @@ %h3 = "Hi, #{@resource.contact}!" %p.lead - = "Please confirm your email address for " - %strong - = "#{@resource.name}." + = "A profile for #{@resource.name} has been successfully created!" + To activate your Profile we need to confirm this email address. %p   %p.callout - Click the link below to confirm your email and to activate your enterprise. This link can be used only once: + Please click the link below to confirm your email and to continue setting up your profile. %br %strong = link_to 'Confirm this email address »', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %p   %p - = "We're so excited that you're joining the #{ Spree::Config[:site_name] }! Don't hestitate to get in touch if you have any questions." + After confirming your email you can access your administration account for this enterprise. + See the + = link_to 'User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' + = "to find out more about #{ Spree::Config[:site_name] }'s features and to start using your profile or online store." = render 'shared/mailers/signoff' diff --git a/app/views/enterprise_mailer/welcome.html.haml b/app/views/enterprise_mailer/welcome.html.haml index 5c69caae77..3cd9d14034 100644 --- a/app/views/enterprise_mailer/welcome.html.haml +++ b/app/views/enterprise_mailer/welcome.html.haml @@ -1,67 +1,27 @@ %h3 = "Welcome, #{@enterprise.contact}!" %p.lead - Congratulations, + Thank you for confirming your email address. %strong - %strong= @enterprise.name + = @enterprise.name = "is now part of #{ Spree::Config.site_name }!" -/ Heading Panel + %p - Please find below all the details for viewing and editing your enterprise on - %strong= "#{ Spree::Config.site_name }." - We suggest keeping this email and information somewhere safe. Logging in with the account details below will allow complete access to your products and services. + The User Guide with detailed support for setting up your Producer or Hub is here: + = link_to 'Open Food Network User Guide', 'http://global.openfoodnetwork.org/platform/user-guide/' --#%p   - --# %p.callout --# %strong --# Your enterprise details --# %table{:width => "100%"} --# %tr --# %td{:align => "right"} --# %strong --# Shop URL --# %td   --# %td --# %a{:href => "#{ main_app.enterprise_shop_url(@enterprise) }", :target => "_blank"} --# = main_app.enterprise_shop_url(@enterprise) --# %tr --# %td   --# %tr --# %td{:align => "right"} --# %strong --# Email --# %td   --# %td --# %a{:href => "mailto:#{ @enterprise.email }", :target => "_blank"} --# = @enterprise.email - -%p   %p - Log into - %strong= "#{ Spree::Config.site_name } Admin" - in order to edit your enterprise details such as website and social media links, or to start adding products to your enterprise! + You can manage your account by logging into the + = link_to 'Admin Panel', spree.admin_url + or by clicking on the cog in the top right hand side of the homepage, and selecting Administration. -%p.callout - %strong - OFN Admin -%table{ :width => "100%"} - %tr - %td{:align => "right"} - %strong - Admin - %td   - %td - %a{:href => "#{ spree.admin_url }", :target => "_blank"} - = spree.admin_url - -%p   -/ /Heading Panel %p - We're so pleased to have you as a valued member of - %strong= "#{Spree::Config.site_name}!" - Don't hestitate to get in touch if you have any questions. + We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. + = link_to 'Join the community.', 'http://community.openfoodnetwork.org/' + +%p + If you have any difficulties, check out our FAQs, browse the forum or post a 'Support' topic and someone will help you out! = render 'shared/mailers/signoff' -= render 'shared/mailers/social_and_contact' \ No newline at end of file += render 'shared/mailers/social_and_contact' diff --git a/app/views/producers/index.html.haml b/app/views/producers/index.html.haml index 0e46795701..e3ae9815e2 100644 --- a/app/views/producers/index.html.haml +++ b/app/views/producers/index.html.haml @@ -1,5 +1,6 @@ -= inject_enterprises -.producers.pad-top{"ng-controller" => "EnterprisesCtrl"} += inject_enterprises + +.producers.pad-top{"ng-controller" => "EnterprisesCtrl", "ng-cloak" => true} .row .small-12.columns.pad-top %h1 Find local producers diff --git a/app/views/shared/_analytics.html.haml b/app/views/shared/_analytics.html.haml index ee9ba69923..16ad08ff5f 100644 --- a/app/views/shared/_analytics.html.haml +++ b/app/views/shared/_analytics.html.haml @@ -1,8 +1,9 @@ -:javascript - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +- if Rails.env.production? + :javascript + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-62912229-1', 'auto'); - ga('send', 'pageview'); + ga('create', 'UA-62912229-1', 'auto'); + ga('send', 'pageview'); diff --git a/app/views/spree/admin/reports/xero_invoices.html.haml b/app/views/spree/admin/reports/xero_invoices.html.haml new file mode 100644 index 0000000000..1ae4e3b279 --- /dev/null +++ b/app/views/spree/admin/reports/xero_invoices.html.haml @@ -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) diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index 82120add82..de142e3cac 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -9,7 +9,7 @@ - else = @order.distributor.next_collection_at - = render "shopping_shared/details" + = render "shopping_shared/details" if current_distributor.present? %fieldset#order_summary{"data-hook" => ""} .row diff --git a/app/views/spree/user_mailer/signup_confirmation.html.haml b/app/views/spree/user_mailer/signup_confirmation.html.haml index fb52f57119..c85b377fa4 100644 --- a/app/views/spree/user_mailer/signup_confirmation.html.haml +++ b/app/views/spree/user_mailer/signup_confirmation.html.haml @@ -21,7 +21,9 @@ %hr/ %p   %p.lead - Thanks for joining the network. We look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + Thanks for joining the network. + If you are a customer, we look forward to introducing you to many fantastic farmers, wonderful food hubs and delicious food! + If you are a producer or food enterprise, we are excited to have you as a part of the network. %p We welcome all your questions and feedback; you can use the %em diff --git a/config/environments/development.rb b/config/environments/development.rb index efa229e33c..200484122a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -31,7 +31,7 @@ Openfoodnetwork::Application.configure do # Show emails using Letter Opener config.action_mailer.delivery_method = :letter_opener - config.action_mailer.default_url_options = { host: "test.com" } + config.action_mailer.default_url_options = { host: "0.0.0.0:3000" } end diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb index 8fc9aa8ec7..80dc11d3aa 100644 --- a/config/initializers/delayed_job.rb +++ b/config/initializers/delayed_job.rb @@ -2,6 +2,10 @@ Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) Delayed::Worker.destroy_failed_jobs = false Delayed::Worker.max_run_time = 15.minutes +# Uncomment the next line if you want jobs to be executed straight away. +# For example you want emails to be opened in your browser while testing. +#Delayed::Worker.delay_jobs = false + # Notify bugsnag when a job fails # Code adapted from http://trevorturk.com/2011/01/25/notify-hoptoad-if-theres-an-exception-in-delayedjob/ class Delayed::Worker diff --git a/config/routes.rb b/config/routes.rb index 5ae63289df..5fd29f6271 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb b/db/migrate/20120327000593_add_addresses_checkouts_indexes.rb old mode 100755 new mode 100644 diff --git a/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb new file mode 100644 index 0000000000..e8841b2c5f --- /dev/null +++ b/db/migrate/20150603001843_add_unique_index_to_enterprise_permalink.rb @@ -0,0 +1,16 @@ +class AddUniqueIndexToEnterprisePermalink < ActiveRecord::Migration + def change + duplicates = Enterprise.group(:permalink).having('count(*) > 1').pluck(:permalink) + duplicates.each { |p| resolve_permalink(p) }; + add_index :enterprises, :permalink, :unique => true + end + + def resolve_permalink(permalink) + conflicting = Enterprise.where(permalink: permalink) + while conflicting.size > 1 do + enterprise = conflicting.pop + enterprise.permalink = nil + enterprise.save + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f6cf50a8fa..e58d74c220 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20150527004427) do +ActiveRecord::Schema.define(:version => 20150603001843) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -323,6 +323,7 @@ ActiveRecord::Schema.define(:version => 20150527004427) do add_index "enterprises", ["confirmation_token"], :name => "index_enterprises_on_confirmation_token", :unique => true add_index "enterprises", ["is_primary_producer", "sells"], :name => "index_enterprises_on_is_primary_producer_and_sells" add_index "enterprises", ["owner_id"], :name => "index_enterprises_on_owner_id" + add_index "enterprises", ["permalink"], :name => "index_enterprises_on_permalink", :unique => true add_index "enterprises", ["sells"], :name => "index_enterprises_on_sells" create_table "exchange_fees", :force => true do |t| diff --git a/lib/open_food_network/order_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb index 2662b176dd..011e8d19fe 100644 --- a/lib/open_food_network/order_and_distributor_report.rb +++ b/lib/open_food_network/order_and_distributor_report.rb @@ -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 diff --git a/lib/open_food_network/referer_parser.rb b/lib/open_food_network/referer_parser.rb new file mode 100644 index 0000000000..b90ef21829 --- /dev/null +++ b/lib/open_food_network/referer_parser.rb @@ -0,0 +1,17 @@ +module OpenFoodNetwork + class RefererParser + def self.path(referer) + parse_uri(referer).andand.path if referer + end + + def self.parse_uri(string) + begin + # TODO: make this operation obsolete by fixing URLs generated by AngularJS + string.sub!('##', '#') + URI(string) + rescue URI::InvalidURIError + nil + end + end + end +end diff --git a/lib/open_food_network/xero_invoices_report.rb b/lib/open_food_network/xero_invoices_report.rb new file mode 100644 index 0000000000..1a7f7bd636 --- /dev/null +++ b/lib/open_food_network/xero_invoices_report.rb @@ -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 diff --git a/spec/controllers/admin/customers_controller_spec.rb b/spec/controllers/admin/customers_controller_spec.rb index 3fc5451b8b..bb2e4888c2 100644 --- a/spec/controllers/admin/customers_controller_spec.rb +++ b/spec/controllers/admin/customers_controller_spec.rb @@ -1,3 +1,5 @@ +require 'spec_helper' + describe Admin::CustomersController, type: :controller do include AuthenticationWorkflow diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 5d72705d8a..4f820bb004 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -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 diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 25a91ae160..27b748cce5 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -45,7 +45,7 @@ feature "Registration", js: true do # Choosing a type expect(page).to have_content 'Last step to add My Awesome Enterprise!' click_link 'producer-panel' - click_button 'Continue' + click_button 'Create Profile' # Enterprise should be created expect(page).to have_content 'Nice one!' diff --git a/spec/lib/open_food_network/referer_parser_spec.rb b/spec/lib/open_food_network/referer_parser_spec.rb new file mode 100644 index 0000000000..13cde6099e --- /dev/null +++ b/spec/lib/open_food_network/referer_parser_spec.rb @@ -0,0 +1,23 @@ +require 'open_food_network/referer_parser' +require 'spec_helper' + +module OpenFoodNetwork + describe RefererParser do + + it "handles requests without referer" do + RefererParser.path(nil).should be_nil + end + + it "handles requests with referer" do + RefererParser.path('http://example.org/').should eq('/') + end + + it "handles requests with invalid referer" do + RefererParser.path('this is not a URI').should be_nil + end + + it "handles requests with known issue of referer" do + RefererParser.path('http://example.org/##invalid-fragment').should eq('/') + end + end +end diff --git a/spec/lib/open_food_network/xero_invoices_report_spec.rb b/spec/lib/open_food_network/xero_invoices_report_spec.rb new file mode 100644 index 0000000000..8551d663a8 --- /dev/null +++ b/spec/lib/open_food_network/xero_invoices_report_spec.rb @@ -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 diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 579965aa7a..bd952f2e9c 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -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) } diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index 4058ba30e1..a61f4e67fc 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -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 diff --git a/spec/support/matchers/table_matchers.rb b/spec/support/matchers/table_matchers.rb index 053562b9e4..411b0b646f 100644 --- a/spec/support/matchers/table_matchers.rb +++ b/spec/support/matchers/table_matchers.rb @@ -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