fb2b4d8a8a
chore: fix api test conventions chore: rubocop -A spec/ chore: more rubocop -A fix failing test rubocop fixes removes helper methods that are in my opinion dead code more rubocop fixes rubocop -a --auto-gen-config
256 lines
9.6 KiB
Ruby
256 lines
9.6 KiB
Ruby
# An OrderArticle represents a single Article that is part of an Order.
|
|
class OrderArticle < ApplicationRecord
|
|
include FindEachWithOrder
|
|
|
|
attr_reader :update_global_price
|
|
|
|
belongs_to :order
|
|
belongs_to :article
|
|
belongs_to :article_price, optional: true
|
|
has_many :group_order_articles, dependent: :destroy
|
|
|
|
validates :order_id, :article_id, presence: true
|
|
validate :article_and_price_exist
|
|
validates :article_id, uniqueness: { scope: :order_id }
|
|
|
|
_ordered_sql = 'order_articles.units_to_order > 0 OR order_articles.units_billed > 0 OR order_articles.units_received > 0'
|
|
scope :ordered, -> { where(_ordered_sql) }
|
|
scope :ordered_or_member, lambda {
|
|
includes(:group_order_articles).where("#{_ordered_sql} OR order_articles.quantity > 0 OR group_order_articles.result > 0")
|
|
}
|
|
|
|
before_create :init_from_balancing
|
|
after_destroy :update_ordergroup_prices
|
|
|
|
def self.ransackable_attributes(_auth_object = nil)
|
|
%w[id order_id article_id quantity tolerance units_to_order]
|
|
end
|
|
|
|
def self.ransackable_associations(_auth_object = nil)
|
|
%w[order article]
|
|
end
|
|
|
|
# This method returns either the ArticlePrice or the Article
|
|
# The first will be set, when the the order is finished
|
|
def price
|
|
article_price || article
|
|
end
|
|
|
|
# latest information on available units
|
|
def units
|
|
return units_received unless units_received.nil?
|
|
return units_billed unless units_billed.nil?
|
|
|
|
units_to_order
|
|
end
|
|
|
|
# Count quantities of belonging group_orders.
|
|
# In balancing this can differ from ordered (by supplier) quantity for this article.
|
|
def group_orders_sum
|
|
quantity = group_order_articles.collect(&:result).sum
|
|
{ quantity: quantity, price: quantity * price.fc_price }
|
|
end
|
|
|
|
# Update quantity/tolerance/units_to_order from group_order_articles
|
|
def update_results!
|
|
if order.open?
|
|
self.quantity = group_order_articles.collect(&:quantity).sum
|
|
self.tolerance = group_order_articles.collect(&:tolerance).sum
|
|
self.units_to_order = calculate_units_to_order(quantity, tolerance)
|
|
enforce_boxfill if order.boxfill?
|
|
save!
|
|
elsif order.finished?
|
|
update_attribute(:units_to_order, group_order_articles.collect(&:result).sum)
|
|
end
|
|
end
|
|
|
|
# Returns how many units of the belonging article need to be ordered given the specified order quantity and tolerance.
|
|
# This is determined by calculating how many units can be ordered from the given order quantity, using
|
|
# the tolerance to order an additional unit if the order quantity is not quiet sufficient.
|
|
# There must always be at least one item in a unit that is an ordered quantity (no units are ever entirely
|
|
# filled by tolerance items only).
|
|
#
|
|
# Example:
|
|
#
|
|
# unit_quantity | quantity | tolerance | calculate_units_to_order
|
|
# --------------+----------+-----------+-----------------------
|
|
# 4 | 0 | 2 | 0
|
|
# 4 | 0 | 5 | 0
|
|
# 4 | 2 | 2 | 1
|
|
# 4 | 4 | 2 | 1
|
|
# 4 | 4 | 4 | 1
|
|
# 4 | 5 | 3 | 2
|
|
# 4 | 5 | 4 | 2
|
|
#
|
|
def calculate_units_to_order(quantity, tolerance = 0)
|
|
unit_size = price.unit_quantity
|
|
units = quantity / unit_size
|
|
remainder = quantity % unit_size
|
|
units += ((remainder > 0) && (remainder + tolerance >= unit_size) ? 1 : 0)
|
|
end
|
|
|
|
# Calculate price for ordered quantity.
|
|
def total_price
|
|
units * price.unit_quantity * price.price
|
|
end
|
|
|
|
# Calculate gross price for ordered qunatity.
|
|
def total_gross_price
|
|
units * price.unit_quantity * price.gross_price
|
|
end
|
|
|
|
def ordered_quantities_different_from_group_orders?(ordered_mark = '!', billed_mark = '?', received_mark = '?')
|
|
if !units_received.nil?
|
|
(units_received * price.unit_quantity) == group_orders_sum[:quantity] ? false : received_mark
|
|
elsif !units_billed.nil?
|
|
(units_billed * price.unit_quantity) == group_orders_sum[:quantity] ? false : billed_mark
|
|
elsif !units_to_order.nil?
|
|
(units_to_order * price.unit_quantity) == group_orders_sum[:quantity] ? false : ordered_mark
|
|
end
|
|
end
|
|
|
|
# redistribute articles over ordergroups
|
|
# quantity Number of units to distribute
|
|
# surplus What to do when there are more articles than ordered quantity
|
|
# :tolerance fill member orders' tolerance
|
|
# :stock move to stock
|
|
# nil nothing; for catching the remaining count
|
|
# update_totals Whether to update group_order and ordergroup totals
|
|
# returns array with counts for each surplus method
|
|
def redistribute(quantity, surplus = [:tolerance], update_totals = true)
|
|
qty_left = quantity
|
|
counts = [0] * surplus.length
|
|
|
|
if surplus.index(:tolerance).nil?
|
|
qty_for_members = [qty_left, self.quantity].min
|
|
else
|
|
qty_for_members = [qty_left, self.quantity + tolerance].min
|
|
counts[surplus.index(:tolerance)] = [0, qty_for_members - self.quantity].max
|
|
end
|
|
|
|
# Recompute
|
|
group_order_articles.each { |goa| goa.save_results! qty_for_members }
|
|
qty_left -= qty_for_members
|
|
|
|
# if there's anything left, move to stock if wanted
|
|
if qty_left > 0 && surplus.index(:stock)
|
|
counts[surplus.index(:stock)] = qty_left
|
|
# 1) find existing stock article with same name, unit, price
|
|
# 2) if not found, create new stock article
|
|
# avoiding duplicate stock article names
|
|
end
|
|
counts[surplus.index(nil)] = qty_left if qty_left > 0 && surplus.index(nil)
|
|
|
|
# Update GroupOrder prices & Ordergroup stats
|
|
# TODO only affected group_orders, and once after redistributing all articles
|
|
if update_totals
|
|
update_ordergroup_prices
|
|
order.ordergroups.each(&:update_stats!)
|
|
end
|
|
|
|
# TODO: notifications
|
|
|
|
counts
|
|
end
|
|
|
|
# Updates order_article and belongings during balancing process
|
|
def update_article_and_price!(order_article_attributes, article_attributes, price_attributes = nil)
|
|
OrderArticle.transaction do
|
|
# Updates self
|
|
update!(order_article_attributes)
|
|
|
|
# Updates article
|
|
article.update!(article_attributes)
|
|
|
|
# Updates article_price belonging to current order article
|
|
if price_attributes.present?
|
|
article_price.attributes = price_attributes
|
|
if article_price.changed?
|
|
# Updates also price attributes of article if update_global_price is selected
|
|
if update_global_price
|
|
article.update!(price_attributes)
|
|
self.article_price = article.article_prices.first and save # Assign new created article price to order article
|
|
else
|
|
# Creates a new article_price if neccessary
|
|
# Set created_at timestamp to order ends, to make sure the current article price isn't changed
|
|
create_article_price!(price_attributes.merge(article_id: article_id, created_at: order.ends)) and save
|
|
end
|
|
|
|
# Updates ordergroup values
|
|
update_ordergroup_prices
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_global_price=(value)
|
|
@update_global_price = [true, '1'].include?(value) ? true : false
|
|
end
|
|
|
|
# @return [Number] Units missing for the last +unit_quantity+ of the article.
|
|
def missing_units
|
|
_missing_units(price.unit_quantity, quantity, tolerance)
|
|
end
|
|
|
|
def missing_units_was
|
|
_missing_units(price.unit_quantity, quantity_was, tolerance_was)
|
|
end
|
|
|
|
# Check if the result of any associated GroupOrderArticle was overridden manually
|
|
def result_manually_changed?
|
|
group_order_articles.any? { |goa| goa.result_manually_changed? }
|
|
end
|
|
|
|
def difference_received_ordered
|
|
(units_received || 0) - units_to_order
|
|
end
|
|
|
|
private
|
|
|
|
def article_and_price_exist
|
|
if !(article = Article.find(article_id)) || article.fc_price.nil?
|
|
errors.add(:article,
|
|
I18n.t('model.order_article.error_price'))
|
|
end
|
|
rescue StandardError
|
|
errors.add(:article, I18n.t('model.order_article.error_price'))
|
|
end
|
|
|
|
# Associate with current article price if created in a finished order
|
|
def init_from_balancing
|
|
return unless order.present? && order.finished?
|
|
|
|
self.article_price = article.article_prices.first
|
|
end
|
|
|
|
def update_ordergroup_prices
|
|
# updates prices of ALL ordergroups - these are actually too many
|
|
# in case of performance issues, update only ordergroups, which ordered this article
|
|
# CAUTION: in after_destroy callback related records (e.g. group_order_articles) are already non-existent
|
|
order.group_orders.each(&:update_price!)
|
|
end
|
|
|
|
# Throws an exception when the changed article decreases the amount of filled boxes.
|
|
def enforce_boxfill
|
|
# Either nothing changes, or the tolerance increases,
|
|
# missing_units decreases and the amount doesn't decrease, or
|
|
# tolerance was moved to quantity. Only then are changes allowed in the boxfill phase.
|
|
delta_q = quantity - quantity_was
|
|
delta_t = tolerance - tolerance_was
|
|
delta_mis = missing_units - missing_units_was
|
|
delta_box = units_to_order - units_to_order_was
|
|
unless (delta_q == 0 && delta_t >= 0) ||
|
|
(delta_mis < 0 && delta_box >= 0 && delta_t >= 0) ||
|
|
(delta_q > 0 && delta_q == -delta_t)
|
|
raise ActiveRecord::RecordNotSaved.new("Change not acceptable in boxfill phase for '#{article.name}', sorry.",
|
|
self)
|
|
end
|
|
end
|
|
|
|
def _missing_units(unit_quantity, quantity, tolerance)
|
|
units = unit_quantity - ((quantity % unit_quantity) + tolerance)
|
|
units = 0 if units < 0
|
|
units = 0 if units == unit_quantity
|
|
units
|
|
end
|
|
end
|