2d0c163f13
see https://github.com/foodcoops/foodsoft/pull/907 for reference and original work by viehlieb Co-authored-by: viehlieb <pf@pragma-shift.net> fix PDF Pdf make explicit deposit in invoices work add ordergroupname to invoice file name mark bold sum for vat exempt foodcoops download multiple group order invoice as zip
235 lines
9.4 KiB
Ruby
235 lines
9.4 KiB
Ruby
# A GroupOrderArticle stores the sum of how many items of an OrderArticle are ordered as part of a GroupOrder.
|
|
# The chronologically order of the Ordergroup - activity are stored in GroupOrderArticleQuantity
|
|
#
|
|
class GroupOrderArticle < ApplicationRecord
|
|
include LocalizeInput
|
|
|
|
belongs_to :group_order
|
|
belongs_to :order_article
|
|
has_many :group_order_article_quantities, dependent: :destroy
|
|
|
|
validates :group_order, :order_article, presence: true
|
|
validates :order_article_id, uniqueness: { scope: :group_order_id } # just once an article per group order
|
|
validate :check_order_not_closed # don't allow changes to closed (aka settled) orders
|
|
validates :quantity, :tolerance, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
|
|
scope :ordered, -> { includes(group_order: :ordergroup).order('groups.name') }
|
|
|
|
localize_input_of :result
|
|
|
|
def self.ransackable_attributes(_auth_object = nil)
|
|
%w[id quantity tolerance result]
|
|
end
|
|
|
|
def self.ransackable_associations(_auth_object = nil)
|
|
%w[order_article group_order]
|
|
end
|
|
|
|
# Setter used in group_order_article#new
|
|
# We have to create an group_order, if the ordergroup wasn't involved in the order yet
|
|
def ordergroup_id=(id)
|
|
self.group_order = GroupOrder.where(order_id: order_article.order_id, ordergroup_id: id).first_or_initialize
|
|
end
|
|
|
|
def ordergroup_id
|
|
group_order&.ordergroup_id
|
|
end
|
|
|
|
# Updates the quantity/tolerance for this GroupOrderArticle by updating both GroupOrderArticle properties
|
|
# and the associated GroupOrderArticleQuantities chronologically.
|
|
#
|
|
# See description of the ordering algorithm in the general application documentation for details.
|
|
def update_quantities(quantity, tolerance)
|
|
logger.debug("GroupOrderArticle[#{id}].update_quantities(#{quantity}, #{tolerance})")
|
|
logger.debug("Current quantity = #{self.quantity}, tolerance = #{self.tolerance}")
|
|
|
|
# When quantity and tolerance are zero, we don't serve any purpose
|
|
if quantity == 0 && tolerance == 0
|
|
logger.debug('Self-destructing since requested quantity and tolerance are zero')
|
|
destroy!
|
|
return
|
|
end
|
|
|
|
# Get quantities ordered with the newest item first.
|
|
quantities = group_order_article_quantities.order('created_on DESC').to_a
|
|
logger.debug("GroupOrderArticleQuantity items found: #{quantities.size}")
|
|
|
|
if quantities.size == 0
|
|
# There is no GroupOrderArticleQuantity item yet, just insert with desired quantities...
|
|
logger.debug('No quantities entry at all, inserting a new one with the desired quantities')
|
|
quantities.push(GroupOrderArticleQuantity.new(group_order_article: self, quantity: quantity,
|
|
tolerance: tolerance))
|
|
self.quantity = quantity
|
|
self.tolerance = tolerance
|
|
else
|
|
# Decrease quantity/tolerance if necessary by going through the existing items and decreasing their values...
|
|
i = 0
|
|
while i < quantities.size && (quantity < self.quantity || tolerance < self.tolerance)
|
|
logger.debug("Need to decrease quantities for GroupOrderArticleQuantity[#{quantities[i].id}]")
|
|
if quantity < self.quantity && quantities[i].quantity > 0
|
|
delta = self.quantity - quantity
|
|
delta = [delta, quantities[i].quantity].min
|
|
logger.debug("Decreasing quantity by #{delta}")
|
|
quantities[i].quantity -= delta
|
|
self.quantity -= delta
|
|
end
|
|
if tolerance < self.tolerance && quantities[i].tolerance > 0
|
|
delta = self.tolerance - tolerance
|
|
delta = [delta, quantities[i].tolerance].min
|
|
logger.debug("Decreasing tolerance by #{delta}")
|
|
quantities[i].tolerance -= delta
|
|
self.tolerance -= delta
|
|
end
|
|
i += 1
|
|
end
|
|
# If there is at least one increased value: insert a new GroupOrderArticleQuantity object
|
|
if quantity > self.quantity || tolerance > self.tolerance
|
|
logger.debug('Inserting a new GroupOrderArticleQuantity')
|
|
quantities.insert(0, GroupOrderArticleQuantity.new(
|
|
group_order_article: self,
|
|
quantity: (quantity > self.quantity ? quantity - self.quantity : 0),
|
|
tolerance: (tolerance > self.tolerance ? tolerance - self.tolerance : 0)
|
|
))
|
|
# Recalc totals:
|
|
self.quantity += quantities[0].quantity
|
|
self.tolerance += quantities[0].tolerance
|
|
end
|
|
end
|
|
|
|
# Check if something went terribly wrong and quantites have not been adjusted as desired.
|
|
if self.quantity != quantity || self.tolerance != tolerance
|
|
raise ActiveRecord::RecordNotSaved.new('Unable to update GroupOrderArticle/-Quantities to desired quantities!',
|
|
self)
|
|
end
|
|
|
|
# Remove zero-only items.
|
|
quantities = quantities.reject { |q| q.quantity == 0 && q.tolerance == 0 }
|
|
|
|
# Save
|
|
transaction do
|
|
quantities.each { |i| i.save! }
|
|
self.group_order_article_quantities = quantities
|
|
save!
|
|
end
|
|
end
|
|
|
|
# Determines how many items of this article the Ordergroup receives.
|
|
# Returns a hash with three keys: :quantity / :tolerance / :total
|
|
#
|
|
# See description of the ordering algorithm in the general application documentation for details.
|
|
def calculate_result(total = nil)
|
|
# return memoized result unless a total is given
|
|
return @calculate_result if total.nil? && !@calculate_result.nil?
|
|
|
|
quantity = tolerance = total_quantity = 0
|
|
|
|
# Get total
|
|
if !total.nil?
|
|
logger.debug "<#{order_article.article.name}> => #{total} (given)"
|
|
elsif order_article.article.is_a?(StockArticle)
|
|
total = order_article.article.quantity
|
|
logger.debug "<#{order_article.article.name}> (stock) => #{total}"
|
|
else
|
|
total = order_article.units_to_order * order_article.price.unit_quantity
|
|
logger.debug "<#{order_article.article.name}> units_to_order #{order_article.units_to_order} => #{total}"
|
|
end
|
|
|
|
if total > 0
|
|
# In total there are enough units ordered. Now check the individual result for the ordergroup (group_order).
|
|
#
|
|
# Get all GroupOrderArticleQuantities for this OrderArticle...
|
|
order_quantities = GroupOrderArticleQuantity.where(group_order_article_id: order_article.group_order_article_ids).order('created_on')
|
|
logger.debug "GroupOrderArticleQuantity records found: #{order_quantities.size}"
|
|
|
|
first_order_first_serve = (FoodsoftConfig[:distribution_strategy] == FoodsoftConfig::DistributionStrategy::FIRST_ORDER_FIRST_SERVE)
|
|
|
|
# Determine quantities to be ordered...
|
|
order_quantities.each do |goaq|
|
|
q = goaq.quantity
|
|
q = [q, total - total_quantity].min if first_order_first_serve
|
|
total_quantity += q
|
|
if goaq.group_order_article_id == id
|
|
logger.debug "increasing quantity by #{q}"
|
|
quantity += q
|
|
end
|
|
break if total_quantity >= total && first_order_first_serve
|
|
end
|
|
|
|
# Determine tolerance to be ordered...
|
|
if total_quantity < total
|
|
logger.debug 'determining additional items to be ordered from tolerance'
|
|
order_quantities.each do |goaq|
|
|
q = [goaq.tolerance, total - total_quantity].min
|
|
total_quantity += q
|
|
if goaq.group_order_article_id == id
|
|
logger.debug "increasing tolerance by #{q}"
|
|
tolerance += q
|
|
end
|
|
break if total_quantity >= total
|
|
end
|
|
end
|
|
|
|
logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
|
|
end
|
|
|
|
# memoize result unless a total is given
|
|
r = { quantity: quantity, tolerance: tolerance, total: quantity + tolerance }
|
|
@calculate_result = r if total.nil?
|
|
r
|
|
end
|
|
|
|
# Returns order result,
|
|
# either calcualted on the fly or fetched from result attribute
|
|
# Result is set when finishing the order.
|
|
def result(type = :total)
|
|
self[:result] || calculate_result[type]
|
|
end
|
|
|
|
# This is used for automatic distribution, e.g., in order.finish! or when receiving orders
|
|
def save_results!(article_total = nil)
|
|
new_result = calculate_result(article_total)[:total]
|
|
update_attribute(:result_computed, new_result)
|
|
update_attribute(:result, new_result)
|
|
end
|
|
|
|
# Returns total price for this individual article
|
|
# Until the order is finished this will be the maximum price or
|
|
# the minimum price depending on configuration. When the order is finished it
|
|
# will be the value depending of the article results.
|
|
def total_price(order_article = self.order_article)
|
|
if order_article.order.open?
|
|
if FoodsoftConfig[:tolerance_is_costly]
|
|
order_article.article.fc_price * (quantity + tolerance)
|
|
else
|
|
order_article.article.fc_price * quantity
|
|
end
|
|
else
|
|
order_article.price.fc_price * result
|
|
end
|
|
end
|
|
|
|
def total_price_without_deposit(order_article = self.order_article)
|
|
if order_article.order.open?
|
|
if FoodsoftConfig[:tolerance_is_costly]
|
|
order_article.price.fc_price_without_deposit * (quantity + tolerance)
|
|
else
|
|
order_article.price.fc_price_without_deposit * quantity
|
|
end
|
|
else
|
|
order_article.price.fc_price_without_deposit * result
|
|
end
|
|
end
|
|
|
|
# Check if the result deviates from the result_computed
|
|
def result_manually_changed?
|
|
result != result_computed unless result.nil?
|
|
end
|
|
|
|
private
|
|
|
|
def check_order_not_closed
|
|
return unless order_article.order.closed?
|
|
|
|
errors.add(:order_article, I18n.t('model.group_order_article.order_closed'))
|
|
end
|
|
end
|