2022-07-13 15:06:46 +02:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Provides methods for Product
|
|
|
|
class ProductService
|
2022-09-13 15:01:55 +02:00
|
|
|
class << self
|
2022-09-14 15:15:47 +02:00
|
|
|
PRODUCTS_PER_PAGE = 12
|
2022-10-05 16:58:40 +02:00
|
|
|
MOVEMENTS_PER_PAGE = 10
|
2022-09-09 13:48:20 +02:00
|
|
|
|
2022-09-26 15:45:52 +02:00
|
|
|
def list(filters, operator)
|
2022-09-13 15:01:55 +02:00
|
|
|
products = Product.includes(:product_images)
|
2022-09-20 15:30:44 +02:00
|
|
|
products = filter_by_active(products, filters)
|
2022-10-03 16:32:32 +02:00
|
|
|
products = filter_by_available(products, filters, operator)
|
2022-09-20 15:30:44 +02:00
|
|
|
products = filter_by_categories(products, filters)
|
|
|
|
products = filter_by_machines(products, filters)
|
|
|
|
products = filter_by_keyword_or_reference(products, filters)
|
2022-09-26 15:45:52 +02:00
|
|
|
products = filter_by_stock(products, filters, operator)
|
2022-09-20 15:47:15 +02:00
|
|
|
products = products_ordering(products, filters)
|
2022-08-04 14:02:19 +02:00
|
|
|
|
2022-09-20 15:30:44 +02:00
|
|
|
total_count = products.count
|
|
|
|
products = products.page(filters[:page] || 1).per(PRODUCTS_PER_PAGE)
|
|
|
|
{
|
|
|
|
data: products,
|
|
|
|
page: filters[:page]&.to_i || 1,
|
|
|
|
total_pages: products.page(1).per(PRODUCTS_PER_PAGE).total_pages,
|
|
|
|
page_size: PRODUCTS_PER_PAGE,
|
|
|
|
total_count: total_count
|
|
|
|
}
|
2022-09-09 17:14:22 +02:00
|
|
|
end
|
2022-09-09 13:48:20 +02:00
|
|
|
|
2022-09-13 15:01:55 +02:00
|
|
|
# amount params multiplied by hundred
|
2022-09-14 15:19:12 +02:00
|
|
|
def amount_multiplied_by_hundred(amount)
|
2022-09-13 15:01:55 +02:00
|
|
|
if amount.present?
|
|
|
|
v = amount.to_f
|
2022-08-04 14:02:19 +02:00
|
|
|
|
2022-09-13 15:01:55 +02:00
|
|
|
return v * 100
|
|
|
|
end
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
# @param product Product
|
|
|
|
# @param stock_movements [{stock_type: string, reason: string, quantity: number|string, order_item_id: number|nil}]
|
|
|
|
def update_stock(product, stock_movements = nil)
|
|
|
|
remaining_stock = { internal: product.stock['internal'], external: product.stock['external'] }
|
|
|
|
product.product_stock_movements_attributes = stock_movements&.map do |movement|
|
|
|
|
quantity = ProductStockMovement::OUTGOING_REASONS.include?(movement[:reason]) ? -movement[:quantity].to_i : movement[:quantity].to_i
|
|
|
|
remaining_stock[movement[:stock_type].to_sym] += quantity
|
|
|
|
{
|
|
|
|
stock_type: movement[:stock_type], reason: movement[:reason], quantity: quantity,
|
|
|
|
remaining_stock: remaining_stock[movement[:stock_type].to_sym], date: DateTime.current, order_item_id: movement[:order_item_id]
|
|
|
|
}
|
|
|
|
end || {}
|
|
|
|
product.stock = remaining_stock
|
2022-10-13 14:42:32 +02:00
|
|
|
notify_on_low_stock(product) if product.low_stock_alert
|
|
|
|
product
|
2022-08-04 14:02:19 +02:00
|
|
|
end
|
2022-08-25 08:52:17 +02:00
|
|
|
|
2022-09-13 15:01:55 +02:00
|
|
|
def create(product_params, stock_movement_params = [])
|
|
|
|
product = Product.new(product_params)
|
2022-09-16 11:45:58 +02:00
|
|
|
product.amount = amount_multiplied_by_hundred(product_params[:amount])
|
2022-09-19 19:09:54 +02:00
|
|
|
update_stock(product, stock_movement_params)
|
|
|
|
product
|
2022-08-04 14:02:19 +02:00
|
|
|
end
|
2022-08-25 08:52:17 +02:00
|
|
|
|
2022-09-13 15:01:55 +02:00
|
|
|
def update(product, product_params, stock_movement_params = [])
|
|
|
|
product_params[:amount] = amount_multiplied_by_hundred(product_params[:amount])
|
|
|
|
product.attributes = product_params
|
|
|
|
update_stock(product, stock_movement_params)
|
|
|
|
product
|
|
|
|
end
|
2022-09-19 15:20:42 +02:00
|
|
|
|
2022-10-11 18:53:12 +02:00
|
|
|
def clone(product, product_params)
|
|
|
|
new_product = product.dup
|
|
|
|
new_product.name = product_params[:name]
|
|
|
|
new_product.sku = product_params[:sku]
|
|
|
|
new_product.is_active = product_params[:is_active]
|
|
|
|
new_product.stock['internal'] = 0
|
|
|
|
new_product.stock['external'] = 0
|
|
|
|
new_product.machine_ids = product.machine_ids
|
|
|
|
new_product.machine_ids = product.machine_ids
|
|
|
|
product.product_images.each do |image|
|
|
|
|
pi = new_product.product_images.build
|
|
|
|
pi.is_main = image.is_main
|
|
|
|
pi.attachment = File.open(image.attachment.file.file)
|
|
|
|
end
|
|
|
|
product.product_files.each do |file|
|
|
|
|
pf = new_product.product_files.build
|
|
|
|
pf.attachment = File.open(file.attachment.file.file)
|
|
|
|
end
|
|
|
|
new_product
|
|
|
|
end
|
|
|
|
|
2022-09-19 15:20:42 +02:00
|
|
|
def destroy(product)
|
|
|
|
used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart')
|
|
|
|
.exists?(orderable: product)
|
2022-10-05 13:50:57 +02:00
|
|
|
raise CannotDeleteProductError, I18n.t('errors.messages.product_in_use') if used_in_order
|
2022-09-19 15:20:42 +02:00
|
|
|
|
|
|
|
ActiveRecord::Base.transaction do
|
|
|
|
orders_with_product = Order.joins(:order_items).where(state: 'cart').where('order_items.orderable': product)
|
|
|
|
orders_with_product.each do |order|
|
|
|
|
::Cart::RemoveItemService.new.call(order, product)
|
|
|
|
end
|
|
|
|
|
|
|
|
product.destroy
|
|
|
|
end
|
|
|
|
end
|
2022-09-20 15:30:44 +02:00
|
|
|
|
2022-10-05 16:58:40 +02:00
|
|
|
def stock_movements(filters)
|
|
|
|
movements = ProductStockMovement.where(product_id: filters[:id]).order(date: :desc)
|
|
|
|
movements = filter_by_stock_type(movements, filters)
|
|
|
|
movements = filter_by_reason(movements, filters)
|
|
|
|
|
|
|
|
total_count = movements.count
|
|
|
|
movements = movements.page(filters[:page] || 1).per(MOVEMENTS_PER_PAGE)
|
|
|
|
{
|
|
|
|
data: movements,
|
|
|
|
page: filters[:page]&.to_i || 1,
|
|
|
|
total_pages: movements.page(1).per(MOVEMENTS_PER_PAGE).total_pages,
|
|
|
|
page_size: MOVEMENTS_PER_PAGE,
|
|
|
|
total_count: total_count
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2022-09-20 15:30:44 +02:00
|
|
|
private
|
|
|
|
|
|
|
|
def filter_by_active(products, filters)
|
|
|
|
return products if filters[:is_active].blank?
|
|
|
|
|
|
|
|
state = filters[:is_active] == 'false' ? [nil, false, true] : true
|
|
|
|
products.where(is_active: state)
|
|
|
|
end
|
|
|
|
|
2022-10-03 16:32:32 +02:00
|
|
|
def filter_by_available(products, filters, operator)
|
|
|
|
return products if filters[:is_available].blank? || filters[:is_available] == 'false'
|
|
|
|
|
|
|
|
filter_by_stock(products, { stock_type: 'external', stock_from: '1' }, operator)
|
|
|
|
end
|
|
|
|
|
2022-09-20 15:30:44 +02:00
|
|
|
def filter_by_categories(products, filters)
|
|
|
|
return products if filters[:categories].blank?
|
|
|
|
|
|
|
|
products.where(product_category_id: filters[:categories].split(','))
|
|
|
|
end
|
|
|
|
|
|
|
|
def filter_by_machines(products, filters)
|
|
|
|
return products if filters[:machines].blank?
|
|
|
|
|
|
|
|
products.includes(:machines_products).where('machines_products.machine_id': filters[:machines].split(','))
|
|
|
|
end
|
|
|
|
|
|
|
|
def filter_by_keyword_or_reference(products, filters)
|
|
|
|
return products if filters[:keywords].blank?
|
|
|
|
|
|
|
|
products.where('sku = :sku OR name ILIKE :query OR description ILIKE :query',
|
|
|
|
{ sku: filters[:keywords], query: "%#{filters[:keywords]}%" })
|
|
|
|
end
|
|
|
|
|
2022-09-26 15:45:52 +02:00
|
|
|
def filter_by_stock(products, filters, operator)
|
2022-09-27 09:43:21 +02:00
|
|
|
return products if filters[:stock_type] == 'internal' && !operator&.privileged?
|
2022-09-26 15:45:52 +02:00
|
|
|
|
2022-09-26 17:18:52 +02:00
|
|
|
if filters[:stock_from].to_i.positive?
|
|
|
|
products = products.where('(stock ->> ?)::int >= ?', filters[:stock_type], filters[:stock_from])
|
|
|
|
end
|
2022-10-03 16:35:54 +02:00
|
|
|
products = products.where('(stock ->> ?)::int <= ?', filters[:stock_type], filters[:stock_to]) if filters[:stock_to].to_i != 0
|
2022-09-20 15:30:44 +02:00
|
|
|
|
|
|
|
products
|
|
|
|
end
|
2022-09-20 15:47:15 +02:00
|
|
|
|
|
|
|
def products_ordering(products, filters)
|
2022-09-20 17:12:45 +02:00
|
|
|
key, order = filters[:sort]&.split('-')
|
2022-09-20 15:47:15 +02:00
|
|
|
key ||= 'created_at'
|
|
|
|
order ||= 'desc'
|
|
|
|
|
2022-09-27 11:50:35 +02:00
|
|
|
if key == 'amount'
|
|
|
|
products.order("COALESCE(amount, 0) #{order.upcase}")
|
|
|
|
else
|
|
|
|
products.order(key => order)
|
|
|
|
end
|
2022-09-20 15:47:15 +02:00
|
|
|
end
|
2022-10-05 16:58:40 +02:00
|
|
|
|
|
|
|
def filter_by_stock_type(movements, filters)
|
|
|
|
return movements if filters[:stock_type].blank? || filters[:stock_type] == 'all'
|
|
|
|
|
|
|
|
movements.where(stock_type: filters[:stock_type])
|
|
|
|
end
|
|
|
|
|
|
|
|
def filter_by_reason(movements, filters)
|
|
|
|
return movements if filters[:reason].blank?
|
|
|
|
|
|
|
|
movements.where(reason: filters[:reason])
|
|
|
|
end
|
2022-10-10 10:00:06 +02:00
|
|
|
|
2022-10-13 14:42:32 +02:00
|
|
|
def notify_on_low_stock(product)
|
2022-10-10 10:00:06 +02:00
|
|
|
return product unless product.low_stock_threshold
|
|
|
|
|
2022-10-14 11:27:59 +02:00
|
|
|
if (product.stock['internal'] <= product.low_stock_threshold) ||
|
|
|
|
(product.stock['external'] <= product.low_stock_threshold)
|
2022-10-10 10:00:06 +02:00
|
|
|
NotificationCenter.call type: 'notify_admin_low_stock_threshold',
|
|
|
|
receiver: User.admins_and_managers,
|
|
|
|
attached_object: product
|
|
|
|
end
|
|
|
|
product
|
|
|
|
end
|
2022-08-25 08:52:17 +02:00
|
|
|
end
|
2022-07-13 15:06:46 +02:00
|
|
|
end
|