Commit 5db57ed9 by Tran Hoang Viet

VietTH: Implement feature stock manager

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