Commit 5db57ed9 by Tran Hoang Viet

VietTH: Implement feature stock manager

parent e86b7398
...@@ -91,3 +91,5 @@ group :development do ...@@ -91,3 +91,5 @@ group :development do
end end
gem 'rubocop', '~> 0.32.1' gem 'rubocop', '~> 0.32.1'
gem 'cartman', '~> 2.1.2'
\ No newline at end of file
...@@ -70,6 +70,8 @@ GEM ...@@ -70,6 +70,8 @@ GEM
activesupport (>= 3.2.0) activesupport (>= 3.2.0)
json (>= 1.7) json (>= 1.7)
mime-types (>= 1.16) mime-types (>= 1.16)
cartman (2.1.2)
redis
celluloid (0.16.0) celluloid (0.16.0)
timers (~> 4.0.0) timers (~> 4.0.0)
coderay (1.1.0) coderay (1.1.0)
...@@ -324,6 +326,7 @@ DEPENDENCIES ...@@ -324,6 +326,7 @@ DEPENDENCIES
capistrano-sidekiq (~> 0.5.2) capistrano-sidekiq (~> 0.5.2)
capistrano3-unicorn capistrano3-unicorn
carrierwave (~> 0.10.0) carrierwave (~> 0.10.0)
cartman (~> 2.1.2)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
database_cleaner (~> 1.4.1) database_cleaner (~> 1.4.1)
devise (~> 3.5.1) devise (~> 3.5.1)
...@@ -359,3 +362,6 @@ DEPENDENCIES ...@@ -359,3 +362,6 @@ DEPENDENCIES
unicorn unicorn
vacuum (~> 1.3.0) vacuum (~> 1.3.0)
web-console (~> 2.0) web-console (~> 2.0)
BUNDLED WITH
1.10.5
...@@ -13,7 +13,10 @@ ...@@ -13,7 +13,10 @@
//= require jquery //= require jquery
//= require jquery_ujs //= require jquery_ujs
//= require jquery.validate.min //= require jquery.validate.min
//= require additional-methods
//= require twitter/bootstrap //= require twitter/bootstrap
//= require jquery_methods
//= require product //= require product
//= require order
//= require main
//= require ready //= require ready
//= require additional-methods
@mainLib = (->
return @
)()
\ No newline at end of file
@orderLib = (->
@validateCheckoutForm = ->
$('#checkout-cart form').validate()
jQuery.validator.addClassRules 'input-quantity', {
required: true
number: true
min: 1
}
$('.input-quantity').each ->
$(this).rules 'add', {max: $(this).data('max')}
return @
)()
\ No newline at end of file
...@@ -11,5 +11,16 @@ ...@@ -11,5 +11,16 @@
required: true required: true
accept: "image/*" accept: "image/*"
@validateQuantity = ->
$('.form-quantity').each ->
$(this).validate
rules:
quantity:
required: true
number: true
min: 1
max: $(this).find('.max-quantity').val()
errorPlacement: -> {}
return @ return @
)() )()
\ No newline at end of file
$(document).ready -> $(document).ready ->
window.productLib.validateForm() window.productLib.validateForm()
window.productLib.validateQuantity()
window.orderLib.validateCheckoutForm()
...@@ -31,10 +31,17 @@ label.error{ ...@@ -31,10 +31,17 @@ label.error{
#content{ #content{
.products-list-detail{ .products-list-detail{
.product-item{ .product-item{
height: 70px; margin-bottom: 30px;
.product-image img{ .product-image{
width: 51px; position: relative;
height: 32px; &.disable{
opacity: 0.4;
}
img{
height: 180px;
min-width: 185px;
margin-bottom: 5px;
}
} }
.product-title{ .product-title{
...@@ -42,18 +49,47 @@ label.error{ ...@@ -42,18 +49,47 @@ label.error{
} }
} }
} }
#recommend-items{
.product-item{ .stock{
height: 130px; display: inline-block;
.product-image img{ bottom: 0;
width: 71px; padding: 7px 10px;
height: 38px; opacity: 1;
color: white;
border: 1px solid #ccc;
height: 35px;
} }
.product-title{ .stock{
margin: 10px 0; background-color: #000;
float: left;
}
.product-addto-cart{
text-align: right;
width: 90px;
float: right;
.quantity{
width: 40px;
height: 35px;
display: inline-block;
text-align: center;
}
.btn-add{
width: 50px;
height: 35px;
display: inline-block;
border-radius: 0;
border-left: 0;
} }
} }
.product-price{
font-size: 20px;
color: #007BFF;
width: 100px;
margin: 0 auto 10px;
background-color: #ddd;
} }
} }
...@@ -73,4 +109,20 @@ footer{ ...@@ -73,4 +109,20 @@ footer{
background-color: #ccc; background-color: #ccc;
padding: 15px 0 0 0; padding: 15px 0 0 0;
height: 45px; height: 45px;
margin-top: 20px;
}
#cart-info{
.list-cart-item{
.item{
float: left;
width: 100%;
padding: 5px 0;
}
}
.total{
font-weight: bold;
margin: 11px 0;
font-size: 20px;
}
} }
\ No newline at end of file
module OrdersManagement extend ActiveSupport::Concern
included do
def add_to_cart product
order_item = {
'id' => product.id,
'title' => product.title,
'price' => product.price,
'quantity' => 1
}
unless session['cart_info']['items'].include?(order_item)
session['cart_info']['items'].push(order_item)
session['cart_info']['total'] += 1
end
end
def clear_cart
session['cart_info'] = nil
end
def session_cart
session['cart_info'].with_indifferent_access
end
end
end
\ No newline at end of file
class OrdersController < ApplicationController class OrdersController < ApplicationController
include OrdersManagement
before_action :authenticate_user! before_action :authenticate_user!
def index def index
@order = Order.new
end end
def create def create
if order_service.create(order_params) result = order_service.create(cart_params)
clear_cart if result[:status].present?
redirect_to(root_path, notice: 'Checkout is successful.') redirect_to(root_path, notice: 'Checkout is successful.')
else else
flash[:alert] = result[:messages]
render :index render :index
end end
end end
private private
def order_params def cart_params
params.require(:order).permit(:total, order_items_attributes: [:product_id, :quantity]) params.require(:cart).permit(items: [:id, :quantity])
end end
def order_service def order_service
......
class ProductsController < ApplicationController class ProductsController < ApplicationController
include OrdersManagement
before_action :set_product, only: [:show, :add_cart] before_action :set_product, only: [:show, :add_cart]
before_action :set_categories, only: [:new] before_action :set_categories, only: [:new]
before_action :add_breadcrumb_home before_action :add_breadcrumb_home
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create, :add_cart]
def new def new
@product = Product.new @product = Product.new
...@@ -26,8 +24,12 @@ class ProductsController < ApplicationController ...@@ -26,8 +24,12 @@ class ProductsController < ApplicationController
end end
def add_cart def add_cart
add_to_cart(@product) result = order_service.add_to_cart(@product)
redirect_to(@product, notice: 'Product is added to cart.') if result[:status].present?
redirect_to :back
else
redirect_to :back, alert: "Quanity must have less than #{@product.stock}"
end
end end
def search def search
...@@ -49,4 +51,8 @@ class ProductsController < ApplicationController ...@@ -49,4 +51,8 @@ class ProductsController < ApplicationController
params.require(:product).permit(:title, :price, :category_id, :image) params.require(:product).permit(:title, :price, :category_id, :image)
end end
def order_service
@order_service ||= OrderService.new(current_user, params)
end
end end
\ No newline at end of file
...@@ -6,15 +6,15 @@ class ProductDecorator < Draper::Decorator ...@@ -6,15 +6,15 @@ class ProductDecorator < Draper::Decorator
end end
def image_sm_url def image_sm_url
object.amazon? ? object.image_sm_url : object.image.small.url (object.amazon? && object.image_sm_url) ? object.image_sm_url : object.image.small.url
end end
def image_md_url def image_md_url
object.amazon? ? object.image_md_url : object.image.medium.url (object.amazon? && object.image_md_url) ? object.image_md_url : object.image.medium.url
end end
def image_lg_url def image_lg_url
object.amazon? ? object.image_lg_url : object.image.large.url (object.amazon? && object.image_lg_url) ? object.image_lg_url : object.image.large.url
end end
def short_title def short_title
......
module ApplicationHelper module ApplicationHelper
def session_cart def cart_valid?
(session[:cart_info] ||= {items: [], total: 0}).with_indifferent_access user_signed_in? && !current_user.cart.count.zero?
end end
end end
class Category < ActiveRecord::Base class Category < ActiveRecord::Base
default_scope { order(:title) } default_scope { order(:title) }
JSON_DEFAULT = %i(id title) JSON_DEFAULT = %i(id title products_count)
has_many :products, dependent: :destroy has_many :products, dependent: :destroy
......
class OrderItem < ActiveRecord::Base class OrderItem < ActiveRecord::Base
belongs_to :order belongs_to :order
belongs_to :product belongs_to :product
before_save :update_product_quantity
def update_product_quantity
self.product.decrement!(:stock, self.quantity)
end
end end
...@@ -6,7 +6,7 @@ class Product < ActiveRecord::Base ...@@ -6,7 +6,7 @@ class Product < ActiveRecord::Base
scope :newest, ->{ self.order(created_at: :desc).limit(Settings.limit_product_newest) } scope :newest, ->{ self.order(created_at: :desc).limit(Settings.limit_product_newest) }
# relations # relations
belongs_to :category belongs_to :category, counter_cache: true
belongs_to :user belongs_to :user
# validates # validates
......
...@@ -8,4 +8,9 @@ class User < ActiveRecord::Base ...@@ -8,4 +8,9 @@ class User < ActiveRecord::Base
# :confirmable, :lockable, :timeoutable and :omniauthable # :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
def cart
Cartman::Cart.new(self.id)
end
end end
class OrderService < BaseService class OrderService < BaseService
def create(order_params) def add_to_cart product
order = current_user.orders.build(order_params) quantity = if current_user.cart.contains?(product)
cart_item = current_user.cart.find(product)
cart_item.destroy
params[:quantity].to_i + cart_item.quantity.to_i
else
params[:quantity].to_i
end
product_ids = [] if quantity <= product.stock
quantites = {} current_user.cart.add_item(
order_params['order_items_attributes'].each do |item| id: product.id,
id = item.last['product_id'] name: product.title,
product_ids << id unit_cost: product.price,
quantites[id] = item.last['quantity'] cost: (product.price || 0) * quantity,
quantity: quantity,
type: Product,
stock: product.stock
)
{status: true}
else
{status: false, message: "Quanity must have less than #{product.stock}"}
end
end end
products = Product.where(id: [product_ids]).select(:id, :price) def create(cart_params)
total = products.inject(0) do |sum, product| result = update_cart_info(cart_params['items'].values)
sum + (product.price.to_i * quantites[product.id.to_s].to_i) if result[:status].present?
order_items_attributes = current_user.cart.items.map do |item|
{product_id: item.id, quantity: item.quantity, price: item.unit_cost}
end end
order.total = total
order = current_user.orders.build(
order_items_attributes: order_items_attributes,
total: current_user.cart.total
)
if order.save if order.save
current_user.cart.destroy!
send_checkout_email(order) send_checkout_email(order)
{status: true} {status: true}
else else
{status: false} {status: false}
end end
else
{status: false, message: result[:messages].join(',')}
end
end end
private private
...@@ -30,4 +54,29 @@ class OrderService < BaseService ...@@ -30,4 +54,29 @@ class OrderService < BaseService
OrderMailer.send_checkout(current_user.email, order).deliver_later OrderMailer.send_checkout(current_user.email, order).deliver_later
end end
def update_cart_info(items)
products = Product.where(items.map { |item| item['id'] })
products = products.inject({}) do |list, product|
list[product.id.to_s] = product
list
end
errors = []
items.each do |item|
product = products[item['id']]
if item['quantity'].to_i > product.stock
errors.push(product.id)
else
cart_item = current_user.cart.find(product)
cart_item.quantity = item['quantity']
end
end
if errors.present?
{status: false, messages: errors}
else
{status: true}
end
end
end end
\ No newline at end of file
...@@ -57,4 +57,8 @@ class ImageUploader < CarrierWave::Uploader::Base ...@@ -57,4 +57,8 @@ class ImageUploader < CarrierWave::Uploader::Base
# "something.jpg" if original_filename # "something.jpg" if original_filename
# end # end
def default_url
ActionController::Base.helpers.asset_path('no-image.jpg')
end
end end
%li %li
= link_to category.decorate.title, category = link_to "#{category.decorate.title} (#{category.decorate.products_count})", category
\ No newline at end of file \ No newline at end of file
- products.each do |product| = render products
.product-item.col-md-4.text-center \ No newline at end of file
.product-title
= link_to product.decorate.short_title, product
.product-image.img-thumbnail
= image_tag product.decorate.image_md_url
.module .module
%h3.title Recommended Items %h3.title Recommended Items
.module .module
#recommend-items #recommend-items.products-list-detail
= render 'categories/recommended', products: @recommended_items = render 'categories/recommended', products: @recommended_items
.clearfix .clearfix
......
#cart-info - if cart_valid?
%h3.title Category #cart-info
= "Total: #{pluralize(session_cart[:total], 'item')}" %h3.title Cart info
= link_to 'Checkout', orders_path, class: 'btn btn-default pull-right' .col-md-9 Title
\ No newline at end of file .col-md-3.text-right Quantity
.list-cart-item
- current_user.cart.items.each do |item|
.item
.col-md-9
= link_to item.name.truncate(25, separator: ' '), product_path(item.id)
.col-md-3.text-right
= item.quantity
.clearfix
.total.col-md-12
Total:
= current_user.cart.total
.checkout.col-md-12
= link_to 'Checkout', orders_path, class: 'btn btn-primary'
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
.col-md-10 .col-md-10
= yield = yield
.clearfix
%footer.footer %footer.footer
.container .container
%p.text-muted Footer %p.text-muted Footer
\ No newline at end of file
#checkout-cart - if cart_valid?
= form_for @order do |f| #checkout-cart
= form_for Order.new do |f|
%table.table %table.table
%thead %thead
%tr %tr
%td.text-right # %td.text-right #
%td Title %td Title
%td Price %td Price
%td Stock
%td Quantity %td Quantity
%tbody %tbody
- session_cart[:items].each_with_index do |order_item, index| - current_user.cart.items.each_with_index do |order_item, index|
%tr %tr
%td.number.text-right %td.number.text-right
= index + 1 = index + 1
%td.title %td.title
= link_to order_item[:title], product_path(order_item[:id]) = link_to order_item.name, product_path(order_item.id)
= hidden_field_tag "order[order_items_attributes][#{index}][product_id]", order_item[:id] = hidden_field_tag "cart[items][#{index}][id]", order_item.id
%td.price %td.price
= order_item[:price] = order_item.unit_cost
%td
= order_item.stock
%td.quantity %td.quantity
= text_field_tag "order[order_items_attributes][#{index}][quantity]", order_item[:quantity] = text_field_tag "cart[items][#{index}][quantity]", order_item.quantity, class: 'form-control compare input-quantity', data: {max: order_item.stock}
%tfoot %tfoot
%tr %tr
%td.text-right{colspan: 4} %td.text-right{colspan: 5}
= f.submit 'Checkout', class: 'btn btn-primary' = f.submit 'Checkout', class: 'btn btn-primary'
.product-addto-cart.input-group
- max_quantity = product.stock - current_user.cart.find(product).try(:quantity).to_i
= form_tag add_cart_product_path(product), method: :get, class: 'form-quantity' do
= text_field_tag :quantity, 1, class: 'form-control quantity'
= hidden_field_tag :stock, max_quantity, class: 'max-quantity'
.input-group-btn
= submit_tag 'Add', class: "btn-add btn btn-default", disabled: product.stock.zero?
.row.product-item .product-item.col-md-4.text-center
.col-md-1
.product-image.img-thumbnail
%img{src: product.decorate.image_sm_url}
.col-md-9
.product-title .product-title
= link_to product.decorate.short_title, product_path(product) = link_to product.decorate.short_title, product
- if local_assigns.has_key?(:show_price) && show_price.present?
.col-md-2
.product-price .product-price
Price:
= product.decorate.price = product.decorate.price
.product-image.img-thumbnail{class: "#{'disable' if product.stock.zero?}"}
= image_tag product.decorate.image_lg_url, class: 'img-reponsive'
.clearfix
%span.stock
Stock:
= product.stock
= render 'products/add_to_cart_btn', product: product
#product-detail #product-detail.products-list-detail
.product-title = render 'product', product: @product
%h4= @product.title
.product-image .clearfix
= image_tag @product.decorate.image_lg_url, class: 'img-thumbnail'
.col-md-4.text-center
.product-id .product-id
Item ID: Item ID:
= @product.id = @product.id
.product-price
Price:
= @product.decorate.price
.product-date .product-date
Date: Date:
= l(@product.created_at) = l(@product.created_at)
.clearfix .clearfix
\ No newline at end of file
.product-addto-cart
= link_to 'Buy', add_cart_product_path(@product), class: 'btn btn-primary'
\ No newline at end of file
...@@ -39,7 +39,7 @@ Rails.application.configure do ...@@ -39,7 +39,7 @@ Rails.application.configure do
# Raises error for missing translations # Raises error for missing translations
# config.action_view.raise_on_missing_translations = true # config.action_view.raise_on_missing_translations = true
# config.action_mailer.delivery_method = :letter_opener config.action_mailer.delivery_method = :letter_opener
config.action_mailer.smtp_settings = { config.action_mailer.smtp_settings = {
:address => "smtp.mandrillapp.com", :address => "smtp.mandrillapp.com",
:port => 587, :port => 587,
...@@ -48,7 +48,7 @@ Rails.application.configure do ...@@ -48,7 +48,7 @@ Rails.application.configure do
} }
# ActionMailer Config # ActionMailer Config
config.action_mailer.default_url_options = { :host => 'localhost', port: 3000 } config.action_mailer.default_url_options = { :host => 'localhost', port: 3000 }
config.action_mailer.delivery_method = :smtp # config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
end end
defaults: &defaults defaults: &defaults
limit_category: 5 limit_category: 5
limit_product_recommended: 6 limit_product_recommended: 6
limit_product_newest: 4 limit_product_newest: 9
limit_length_category_title: 50 limit_length_category_title: 50
development: development:
......
class AddStockToProducts < ActiveRecord::Migration
def change
add_column :products, :stock, :integer, default: 0
end
end
class AddProductsCountToCategories < ActiveRecord::Migration
def change
add_column :categories, :products_count, :integer, default: 0
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150708092744) do ActiveRecord::Schema.define(version: 20150713093724) do
create_table "categories", force: :cascade do |t| create_table "categories", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
...@@ -19,6 +19,7 @@ ActiveRecord::Schema.define(version: 20150708092744) do ...@@ -19,6 +19,7 @@ ActiveRecord::Schema.define(version: 20150708092744) do
t.string "node_id", limit: 255 t.string "node_id", limit: 255
t.string "title", limit: 255 t.string "title", limit: 255
t.integer "category_type", limit: 4, default: 0 t.integer "category_type", limit: 4, default: 0
t.integer "products_count", limit: 4, default: 0
end end
create_table "order_items", force: :cascade do |t| create_table "order_items", force: :cascade do |t|
...@@ -55,6 +56,7 @@ ActiveRecord::Schema.define(version: 20150708092744) do ...@@ -55,6 +56,7 @@ ActiveRecord::Schema.define(version: 20150708092744) do
t.integer "product_type", limit: 4, default: 0 t.integer "product_type", limit: 4, default: 0
t.string "image", limit: 255 t.string "image", limit: 255
t.integer "user_id", limit: 4 t.integer "user_id", limit: 4
t.integer "stock", limit: 4, default: 0
end end
add_index "products", ["category_id"], name: "index_products_on_category_id", using: :btree add_index "products", ["category_id"], name: "index_products_on_category_id", using: :btree
......
...@@ -44,6 +44,7 @@ namespace :aws do ...@@ -44,6 +44,7 @@ namespace :aws do
product.image_md_url = item['MediumImage'].try(:[], 'URL') product.image_md_url = item['MediumImage'].try(:[], 'URL')
product.image_sm_url = item['SmallImage'].try(:[], 'URL') product.image_sm_url = item['SmallImage'].try(:[], 'URL')
product.price = item['ItemAttributes']['ListPrice'].try(:[], 'Amount').try(:to_i) product.price = item['ItemAttributes']['ListPrice'].try(:[], 'Amount').try(:to_i)
product.stock = 5 #item['ItemAttributes']['BinItemCount'].to_i
product.product_type = Product.product_types[:amazon] product.product_type = Product.product_types[:amazon]
product.user = User.first product.user = User.first
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment