Add a simple estimate when the stock is reaching zero...
This commit is contained in:
parent
5c32cd892d
commit
e88fbb9d8e
26
LICENSE
26
LICENSE
|
@ -1,26 +1,4 @@
|
||||||
Copyright (c) 2013 [name of plugin creator]
|
Copyright (c) 2013 s.f.m.c. GmbH
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
AGPLv3 The code is AGPLv3
|
||||||
are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
* Neither the name Spree nor the names of its contributors may be used to
|
|
||||||
endorse or promote products derived from this software without specific
|
|
||||||
prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
||||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
||||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
10
README.md
10
README.md
|
@ -1,13 +1,15 @@
|
||||||
SpreeSysmocomStock
|
SysmocomStock
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Introduction goes here.
|
Help with estimating when the stock will turn empty. More varpoware
|
||||||
|
than actual functions but it could take lead-time of a product into
|
||||||
|
account.. propose the quantity..
|
||||||
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
=======
|
=======
|
||||||
|
|
||||||
Example goes here.
|
TODO
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
@ -18,4 +20,4 @@ Be sure to bundle your dependencies and then create a dummy test app for the spe
|
||||||
$ bundle exec rake test_app
|
$ bundle exec rake test_app
|
||||||
$ bundle exec rspec spec
|
$ bundle exec rspec spec
|
||||||
|
|
||||||
Copyright (c) 2013 [name of extension creator], released under the New BSD License
|
Copyright (c) 2013 sysmocom s.f.m.c. GmbH, released under AGPLv3 or later
|
||||||
|
|
2
Rakefile
2
Rakefile
|
@ -10,6 +10,6 @@ task :default => [:spec]
|
||||||
|
|
||||||
desc 'Generates a dummy app for testing'
|
desc 'Generates a dummy app for testing'
|
||||||
task :test_app do
|
task :test_app do
|
||||||
ENV['LIB_NAME'] = 'spree_sysmocom_stock'
|
ENV['LIB_NAME'] = 'sysmocom_stock'
|
||||||
Rake::Task['common:test_app'].invoke
|
Rake::Task['common:test_app'].invoke
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
#
|
#
|
||||||
# '1.2.x' => { :branch => 'master' }
|
'1.2.x' => { :branch => 'master' }
|
||||||
# '1.1.x' => { :branch => '1-1-stable' }
|
# '1.1.x' => { :branch => '1-1-stable' }
|
||||||
# '1.0.x' => { :branch => '1-0-stable' }
|
# '1.0.x' => { :branch => '1-0-stable' }
|
||||||
# '0.70.x' => { :branch => '0-70-stable' }
|
# '0.70.x' => { :branch => '0-70-stable' }
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
module Spree
|
||||||
|
class StockMailer < ActionMailer::Base
|
||||||
|
helper 'spree/base'
|
||||||
|
|
||||||
|
def find_empty_products()
|
||||||
|
Spree::Product.where(:count_on_hand => 0).order("name").select {|prod|
|
||||||
|
prod.deleted_at.nil? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_empty_variants()
|
||||||
|
Spree::Variant.where(:count_on_hand => 0).order("sku").select {|var|
|
||||||
|
var.deleted_at.nil? and not var.is_master?
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_estimate(var)
|
||||||
|
shipped_week = Spree::InventoryUnit.find(:all, :conditions => {
|
||||||
|
:state => 'shipped',
|
||||||
|
:variant_id => var.id,
|
||||||
|
:updated_at => @last_week.midnight..@today.end_of_day,
|
||||||
|
}).size()
|
||||||
|
shipped_month = Spree::InventoryUnit.find(:all, :conditions => {
|
||||||
|
:state => 'shipped',
|
||||||
|
:variant_id => var.id,
|
||||||
|
:updated_at => @last_month.midnight..@today.end_of_day,
|
||||||
|
}).size()
|
||||||
|
shipped_year = Spree::InventoryUnit.find(:all, :conditions => {
|
||||||
|
:state => 'shipped',
|
||||||
|
:variant_id => var.id,
|
||||||
|
:updated_at => @last_year.midnight..@today.end_of_day,
|
||||||
|
}).size()
|
||||||
|
|
||||||
|
# Normalize to units per day... with a shared secret about the range
|
||||||
|
norm_week = shipped_week / 7.0
|
||||||
|
norm_month = shipped_month / 30.0
|
||||||
|
norm_year = shipped_year / 365.0
|
||||||
|
|
||||||
|
# A weighted mean with magic numbers pulled out of thing air.
|
||||||
|
mean = ((0.6 * norm_week) + (0.25 * norm_month) + (0.15 * norm_year)) / 1.0
|
||||||
|
if var.sku.empty?
|
||||||
|
name = "Prd " + var.product.sku
|
||||||
|
else
|
||||||
|
name = "Sku " + var.sku
|
||||||
|
end
|
||||||
|
|
||||||
|
{'weighted_mean' => mean, 'variant' => var, 'product' => var.product, 'name' => name}
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_forecast()
|
||||||
|
@today = Date.today
|
||||||
|
@last_week = @today - 7
|
||||||
|
@last_month = @today - 30
|
||||||
|
@last_year = @today - 365
|
||||||
|
|
||||||
|
forecast = []
|
||||||
|
vars = Spree::Variant.where("deleted_at IS NULL").order("sku").select {|var|
|
||||||
|
var.product.deleted_at.nil? and var.count_on_hand > 0 }
|
||||||
|
|
||||||
|
vars.each {|var|
|
||||||
|
forecast.push(generate_estimate(var))
|
||||||
|
}
|
||||||
|
|
||||||
|
forecast
|
||||||
|
end
|
||||||
|
|
||||||
|
def stock_report_email()
|
||||||
|
@empty_products = find_empty_products()
|
||||||
|
@empty_variants = find_empty_variants()
|
||||||
|
@forecast = generate_forecast()
|
||||||
|
mail(:to => 'webshop@sysmocom.de',
|
||||||
|
:subject => 'Stock report of the week')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,21 @@
|
||||||
|
Dear Shop-Owner,
|
||||||
|
|
||||||
|
below is the current stock report and estimates on when the products
|
||||||
|
will run low.
|
||||||
|
|
||||||
|
Estimates:
|
||||||
|
<% @forecast.each do |dict| %>
|
||||||
|
<%= dict['name'] %> => <%= dict['weighted_mean'].round(2) %> days until sold out.
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
Empty Products:
|
||||||
|
<% @empty_products.each do |product| %>
|
||||||
|
<%= product.name %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
Empty Variants:
|
||||||
|
<% @empty_variants.each do |variant| %>
|
||||||
|
<%= variant.name %>
|
||||||
|
<% end %>
|
|
@ -1,19 +1,19 @@
|
||||||
module SpreeSysmocomStock
|
module SysmocomStock
|
||||||
module Generators
|
module Generators
|
||||||
class InstallGenerator < Rails::Generators::Base
|
class InstallGenerator < Rails::Generators::Base
|
||||||
|
|
||||||
def add_javascripts
|
def add_javascripts
|
||||||
append_file 'app/assets/javascripts/store/all.js', "//= require store/spree_sysmocom_stock\n"
|
append_file 'app/assets/javascripts/store/all.js', "//= require store/sysmocom_stock\n"
|
||||||
append_file 'app/assets/javascripts/admin/all.js', "//= require admin/spree_sysmocom_stock\n"
|
append_file 'app/assets/javascripts/admin/all.js', "//= require admin/sysmocom_stock\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_stylesheets
|
def add_stylesheets
|
||||||
inject_into_file 'app/assets/stylesheets/store/all.css', " *= require store/spree_sysmocom_stock\n", :before => /\*\//, :verbose => true
|
inject_into_file 'app/assets/stylesheets/store/all.css', " *= require store/sysmocom_stock\n", :before => /\*\//, :verbose => true
|
||||||
inject_into_file 'app/assets/stylesheets/admin/all.css', " *= require admin/spree_sysmocom_stock\n", :before => /\*\//, :verbose => true
|
inject_into_file 'app/assets/stylesheets/admin/all.css', " *= require admin/sysmocom_stock\n", :before => /\*\//, :verbose => true
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_migrations
|
def add_migrations
|
||||||
run 'bundle exec rake railties:install:migrations FROM=spree_sysmocom_stock'
|
run 'bundle exec rake railties:install:migrations FROM=sysmocom_stock'
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_migrations
|
def run_migrations
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
require 'spree_core'
|
require 'spree_core'
|
||||||
require 'spree_sysmocom_stock/engine'
|
require 'sysmocom_stock/engine'
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
module SpreeSysmocomStock
|
module SysmocomStock
|
||||||
class Engine < Rails::Engine
|
class Engine < Rails::Engine
|
||||||
require 'spree/core'
|
require 'spree/core'
|
||||||
isolate_namespace Spree
|
isolate_namespace Spree
|
||||||
engine_name 'spree_sysmocom_stock'
|
engine_name 'sysmocom_stock'
|
||||||
|
|
||||||
config.autoload_paths += %W(#{config.root}/lib)
|
config.autoload_paths += %W(#{config.root}/lib)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
||||||
|
|
||||||
ENGINE_ROOT = File.expand_path('../..', __FILE__)
|
ENGINE_ROOT = File.expand_path('../..', __FILE__)
|
||||||
ENGINE_PATH = File.expand_path('../../lib/spree_sysmocom_stock/engine', __FILE__)
|
ENGINE_PATH = File.expand_path('../../lib/sysmocom_stock/engine', __FILE__)
|
||||||
|
|
||||||
require 'rails/all'
|
require 'rails/all'
|
||||||
require 'rails/engine/commands'
|
require 'rails/engine/commands'
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
# encoding: UTF-8
|
# encoding: UTF-8
|
||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.platform = Gem::Platform::RUBY
|
s.platform = Gem::Platform::RUBY
|
||||||
s.name = 'spree_sysmocom_stock'
|
s.name = 'sysmocom_stock'
|
||||||
s.version = '1.2.4.beta'
|
s.version = '0.0.1'
|
||||||
s.summary = 'TODO: Add gem summary here'
|
s.summary = 'sysmocom s.f.m.c. Stock Management'
|
||||||
s.description = 'TODO: Add (optional) gem description here'
|
s.description = 'Help estimating empty stock, turnover'
|
||||||
s.required_ruby_version = '>= 1.8.7'
|
s.required_ruby_version = '>= 1.8.7'
|
||||||
|
|
||||||
# s.author = 'You'
|
s.author = 'Holger Hans Peter Freyther'
|
||||||
# s.email = 'you@example.com'
|
#s.email = 'you@example.com'
|
||||||
# s.homepage = 'http://www.spreecommerce.com'
|
s.homepage = 'http://www.sysmocom.de'
|
||||||
|
|
||||||
#s.files = `git ls-files`.split("\n")
|
s.files = `git ls-files`.split("\n")
|
||||||
#s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
||||||
s.require_path = 'lib'
|
s.require_path = 'lib'
|
||||||
s.requirements << 'none'
|
s.requirements << 'none'
|
||||||
|
|
||||||
s.add_dependency 'spree_core', '~> 1.2.4.beta'
|
s.add_dependency 'spree_core', '>= 1.2.0'
|
||||||
|
|
||||||
s.add_development_dependency 'capybara', '1.0.1'
|
s.add_development_dependency 'capybara', '1.0.1'
|
||||||
s.add_development_dependency 'factory_girl', '~> 2.6.4'
|
s.add_development_dependency 'factory_girl', '~> 2.6.4'
|
||||||
|
|
Loading…
Reference in New Issue